#!/usr/bin/python3

# SPDX-FileCopyrightText: Red Hat, Inc.
# SPDX-License-Identifier: GPL-2.0-or-later

import argparse
import ipaddress
import os
import platform
import shutil
import subprocess
import sys

DEFAULT_PORTAL = (ipaddress.ip_address("0.0.0.0"), 3260)


def main():
    parser = argparse.ArgumentParser(description="Manage iSCSI targets")

    parser.add_argument(
        "action",
        choices=("create", "delete"),
        help="Action to take.")

    parser.add_argument(
        "target_name",
        help="Target name.")

    parser.add_argument(
        "-s", "--lun-size",
        type=int,
        default=100,
        help="LUN size in GiB (default 100).")

    parser.add_argument(
        "-n", "--lun-count",
        type=int,
        default=10,
        help="Number of LUNs (default 10).")

    parser.add_argument(
        "-r", "--root-dir",
        default="/target",
        help="Root directory (default /target).")

    parser.add_argument(
        "-i", "--iqn-base",
        default="iqn.2003-01.org",
        help="IQN base name (default iqn.2003-01.org).")

    parser.add_argument(
        "--cache",
        action="store_true",
        help="Enable write cache. Enabling write cache improves performance "
             "but increases the chance of data loss. May cause trouble when "
             "running many services on the same server, but works fine if "
             "server is used only for storage. (default False).")

    parser.add_argument(
        "--exists",
        action="store_true",
        help="Allow creating target with existing directory. This is useful "
             "when you attach a disk with existing filesystem to a new VM, "
             "and want to create a new target using the existing directory. "
             "You must create the target using the same configuration as "
             "as the existing directory (default False).")

    parser.add_argument(
        "--portal",
        action="append",
        type=portal,
        help="Create portal for this target. Specify multiple values "
             "to create multiple portals. Value can be IPv4 address "
             "(1.2.3.4), IPv6 address ([fe80::5054:ff:fede:9c26]), or "
             "address:port (1.2.3.4:3260). If not specificed use the"
             "detault portal (0.0.0.0:3260). The default portal does"
             "not work with existing targets with non default portals.")

    parser.add_argument(
        "--tpgt",
        type=int,
        default=1,
        help="Target portal group tag (default 1).")

    args = parser.parse_args()

    host_name = platform.node()
    if "." in host_name:
        host_name = host_name.split(".", 1)[0]

    target_iqn = args.iqn_base + "." + host_name + "." + args.target_name
    target_dir = os.path.join(args.root_dir, args.target_name)

    if args.portal:
        # Duplicate portals are user error.
        if len(args.portal) != len(set(args.portal)):
            raise ValueError(f"Duplicate portals: {args.portal}")
    else:
        # This works only if all other targets use the default portal. Once you
        # add a target using non default portal you must specify the portal for
        # new targets. If you don't care about multiple portals, this default
        # make it easy to use.
        args.portal = [DEFAULT_PORTAL]

    if args.action == "create":
        create_target(args, target_iqn, target_dir, exists=args.exists)
    else:
        delete_target(args, target_iqn, target_dir)


def portal(s):
    _, port = DEFAULT_PORTAL
    if s[0] == "[":
        # IPv6: "[address]" or "[address]:port"
        end = s.find("]")
        if end == -1:
            raise ValueError(s)
        address = s[1:end]
        if end + 1 < len(s):
            if s[end+1] != ":":
                raise ValueError(s)
            port = s[end+2:]
    else:
        # IPv4: address or address:port
        if ":" in s:
            address, port = s.split(":", 1)
        else:
            address = s
    return ipaddress.ip_address(address), int(port)


def format_portal(p):
    address, port = p
    if address.version == 6:
        return f"[{address}]:{port}"
    else:
        return f"{address}:{port}"


def create_target(args, target_iqn, target_dir, exists=False):
    print()
    print("Creating target")
    print("  target_name:   %s" % args.target_name)
    print("  target_iqn:    %s" % target_iqn)
    print("  target_dir:    %s" % target_dir)
    print("  lun_count:     %s" % args.lun_count)
    print("  lun_size:      %s GiB" % args.lun_size)
    print("  cache:         %s" % args.cache)
    print("  exists:        %s" % args.exists)
    print("  portals:       %s" % ", ".join(
        format_portal(p) for p in args.portal))
    print("  tpgt:          %s" % args.tpgt)
    print()

    if not confirm("Create target? [N/y]: "):
        sys.exit(0)

    print("Creating target directory %r" % target_dir)
    os.makedirs(target_dir, exist_ok=exists)

    print("Creating target %r" % target_iqn)
    subprocess.check_call(["targetcli", "/iscsi", "create", target_iqn])

    iqn_path = "/iscsi/%s" % target_iqn
    tpg_path = iqn_path + "/tpg%s" % args.tpgt

    # New target always use tpgt=1. If the user want a different tpgt, we need
    # to remove /tpg1 and create a new one instead.
    if args.tpgt != 1:
        subprocess.check_call(["targetcli", iqn_path, "delete", "tpg1"])
        subprocess.check_call(
            ["targetcli", iqn_path, "create", "tpg%s" % args.tpgt])

    portals_path = tpg_path + "/portals"

    print("Checking if target has the default portal")
    cp = subprocess.run(
        ["targetcli", portals_path, "ls", format_portal(DEFAULT_PORTAL)])
    if cp.returncode == 0:
        if DEFAULT_PORTAL in args.portal:
            args.portal.remove(DEFAULT_PORTAL)
        else:
            print("Removing the default portal")
            address, port = DEFAULT_PORTAL
            subprocess.check_call(
                ["targetcli", portals_path, "remove", str(address), str(port)])

    for address, port in args.portal:
        print("Creating portal address=%s port=%s" % (address, port))
        subprocess.check_call(
            ["targetcli", portals_path, "create", str(address), str(port)])

    print("Setting permissions (any host can access this target)")
    subprocess.check_call(["targetcli", tpg_path, "set", "attribute",
                           "authentication=0",
                           "demo_mode_write_protect=0",
                           "generate_node_acls=1",
                           "cache_dynamic_acls=1"])


    print("Creating disks")

    fileio_path = "/backstores/fileio"
    luns_path = tpg_path + "/luns"
    write_back = "write_back={}".format("true" if args.cache else "false")

    for n in range(args.lun_count):
        file_name = "%02d" % n
        file_path = os.path.join(target_dir, file_name)
        backstore_name = args.target_name + "-" + file_name
        backstore_path = os.path.join(fileio_path, backstore_name)

        print("Creating backing file %r" % file_path)
        subprocess.check_call(["truncate", "-s", "%dG" % args.lun_size, file_path])

        print("Creating backing store %r" % backstore_path)
        # Using write_back=false to enable write-thru mode. This disables caching
        # but lowers the chance for data loss. I also experienced lower stability
        # with write_back=true.
        subprocess.check_call(["targetcli", fileio_path, "create", backstore_name,
                               file_path, write_back])

        subprocess.check_call(["targetcli", backstore_path, "set", "attribute",
                               # Enable Thin Provisioning Unmap (blkdiscard).
                               "emulate_tpu=1",
                               # Enable Thin Provisioning Write Same
                               # (blkdiscard -z).
                               "emulate_tpws=1",
                               # Fix write same limit, client see this as 32M, but
                               # default value is 4096.
                               "max_write_same_len=65335"])

        print("Adding lun for %r" % backstore_path)
        subprocess.check_call(["targetcli", luns_path, "create", backstore_path])

    print("Saving configuration")
    subprocess.check_call(["targetcli", "saveconfig"])

    print("Target added successfully")


def delete_target(args, target_iqn, target_dir):
    print()
    print("Deleting target")
    print("  target_name:   %s" % args.target_name)
    print("  target_iqn:    %s" % target_iqn)
    print("  target_dir:    %s" % target_dir)
    print("  lun_count:     %s" % args.lun_count)
    print()

    if not confirm("Delete target? [N/y]: "):
        sys.exit(0)

    print("Deleting disks")
    fileio_path = "/backstores/fileio"

    for n in range(args.lun_count):
        file_name = "%02d" % n
        file_path = os.path.join(target_dir, file_name)
        backstore_name = args.target_name + "-" + file_name
        backstore_path = os.path.join(fileio_path, backstore_name)

        print("Deleting backing store %r" % backstore_path)
        subprocess.check_call(["targetcli", fileio_path, "delete", backstore_name])

    print("Deleting target %r" % target_iqn)
    subprocess.check_call(["targetcli", "/iscsi", "delete", target_iqn])

    print("Removing target directory %r" % target_dir)
    shutil.rmtree(target_dir)

    print("Saving configuration")
    subprocess.check_call(["targetcli", "saveconfig"])

    print("Target deleted successfully")


def confirm(msg):
    reply = input(msg)
    return reply.strip().lower() == "y"


if __name__ == "__main__":
    main()
